// @ts-nocheck import { PermissionAction } from '@supabase/shared-types/out/constants' import { IS_PLATFORM, useParams } from 'common' import { isEqual } from 'lodash' import { AlertCircle, CornerDownLeft, Loader2 } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { LogoLoader } from 'ui' import { DeployEdgeFunctionWarningModal } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal' import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FileExplorerAndEditor } from '@/components/ui/FileExplorerAndEditor' import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types' import { useEdgeFunctionBodyQuery } from '@/data/edge-functions/edge-function-body-query' import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query' import { useEdgeFunctionDeployMutation } from '@/data/edge-functions/edge-functions-deploy-mutation' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { BASE_PATH } from '@/lib/constants' const CodePage = () => { const { ref, functionSlug } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() const [showDeployWarning, setShowDeployWarning] = useState(false) const { can: canDeployFunction } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug, }) const { data: functionBody, isPending: isLoadingFiles, isError: isErrorLoadingFiles, isSuccess: isSuccessLoadingFiles, error: filesError, } = useEdgeFunctionBodyQuery( { projectRef: ref, slug: functionSlug, }, { // [Alaister]: These parameters prevent the function files // from being refetched when the user is editing the code retry: false, retryOnMount: false, refetchOnWindowFocus: false, staleTime: Infinity, refetchOnMount: false, refetchOnReconnect: false, refetchInterval: false, refetchIntervalInBackground: false, } ) const [files, setFiles] = useState([]) const initialFiles = useMemo(() => { return !!functionBody ? formatFunctionBodyToFiles({ functionBody, entrypointPath: selectedFunction?.entrypoint_path, }) : [] }, [functionBody, selectedFunction?.entrypoint_path]) const { mutate: deployFunction, isPending: isDeploying } = useEdgeFunctionDeployMutation({ onSuccess: () => { toast.success('Successfully updated edge function') setShowDeployWarning(false) setFiles((files) => files.map((f) => { return { ...f, state: 'unchanged' } }) ) }, }) const fileExists = (filePath: string | undefined): boolean => { return filePath ? files.some((file) => file.name === filePath) : false } const onUpdate = async () => { if (isDeploying || !ref || !functionSlug || !selectedFunction || files.length === 0) return try { const entrypoint_path = functionBody?.metadata?.deno2_entrypoint_path ?? selectedFunction.entrypoint_path const newEntrypointPath = entrypoint_path?.split('/').pop() const newImportMapPath = selectedFunction.import_map_path?.split('/').pop() const entrypointExists = fileExists(newEntrypointPath) const importMapExists = fileExists(newImportMapPath) deployFunction({ projectRef: ref, slug: selectedFunction.slug, metadata: { name: selectedFunction.name, verify_jwt: selectedFunction.verify_jwt, ...(entrypointExists && { entrypoint_path: newEntrypointPath }), ...(importMapExists && { import_map_path: newImportMapPath }), }, files: files.map(({ name, content }) => ({ name, content })), }) } catch (error) { toast.error( `Failed to update function: ${error instanceof Error ? error.message : 'Unknown error'}` ) } } const handleDeployClick = () => { if (files.length === 0 || isLoadingFiles) return setShowDeployWarning(true) sendEvent({ action: 'edge_function_deploy_updates_button_clicked', groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown', }, }) } const handleDeployConfirm = () => { sendEvent({ action: 'edge_function_deploy_updates_confirm_clicked', groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown', }, }) onUpdate() } useEffect(() => { if (initialFiles.length === 0) return setFiles(initialFiles) }, [initialFiles]) const hasUnsavedChanges = useMemo(() => { const normalizeFiles = (list: FileData[]) => list.map(({ id, name, content }) => ({ id, name, content })) return !isEqual(normalizeFiles(initialFiles), normalizeFiles(files)) }, [initialFiles, files]) return (
{isLoadingFiles && (
)} {isErrorLoadingFiles && (

Failed to load function code

{filesError?.message || 'There was an error loading the function code. The format may be invalid or the function may be corrupted.'}

)} {isSuccessLoadingFiles && ( <> { const formattedFiles: FileData[] = files.map((f) => { const originalFile = initialFiles.find((x) => x.id === f.id) if (!originalFile) { return f } else if (originalFile.name !== f.name) { return { ...f, state: 'new' } } else if (originalFile.content !== f.content) { return { ...f, state: 'modified' } } return { ...f, state: 'unchanged' } }) setFiles(formattedFiles) }} aiEndpoint={`${BASE_PATH}/api/ai/code/complete`} aiMetadata={{ projectRef: project?.ref, connectionString: project?.connectionString, orgSlug: org?.slug, }} /> {IS_PLATFORM && (
) : (
) } tooltip={{ content: { side: 'top', text: !canDeployFunction ? 'You need additional permissions to update edge functions' : undefined, }, }} > Deploy updates
)} )} setShowDeployWarning(false)} onConfirm={handleDeployConfirm} isDeploying={isDeploying} />
) } CodePage.getLayout = (page: React.ReactNode) => { return ( {page} ) } export default CodePage